import random
import math

import cocos.actions

from gamelib import pathfinding, los, modal

class Action(object):
    # Actions have several properties:
    # type  -- for maybe grouping
    # name  -- (type, name) must be unique
    # label -- for the action selector (popup menu at present)
    # description -- maybe a tooltip thing
    # chance -- the chance (between 0 and 1) of success
    # cost -- the cost of performing this action in action points
    # for_friendly -- whether the action is intended to be performed on a friendly character
    for_friendly = False

    def check(self, level, character, cell, other):
        '''Initialise this action with the given data and also determine
        whether the character can perform this action on the cell or
        other in the level.

        The base implementation stores off the information and checks the
        cost.

        Return boolean
        '''
        self.level = level
        self.character = character
        self.cell = cell
        self.other = other

        # and the chance it will succeed - value between 0 and 1
        self.chance = 1

        if character.actions_remaining < self.cost:
            print 'Action (%s): AP %s too low (need %s)'%(self.__class__.__name__,
                character.actions_remaining, self.cost)
            return False
        return True

    def do(self, callback=None):
        '''Have character perform this action on the cell or other in
        the level.
        '''
        if callback:
            callback()

    def adjacent_check(self):
        '''Check that the target cell is adjacent and accessible to character.
        '''
        # check for too far away
        i, j = self.character.position
        if self.other:
            oi, oj = self.other.position
        else:
            oi, oj = self.cell.position
        if abs(i-oi) > 1 or abs(j-oj) > 1:
            print 'adjacent_check: other is too far away'
            return False

        # check line-of-sight blocking
        char_cell = self.level.ground.get_cell(i, j)
        side = (oi-i, oj-j)
        if side == (0, 0):
            print 'adjacent_check: is SELF!'
            return True

        return True

    def range_check(self):
        '''Check that "other" or "cell" is targettable by character, and
        return the range. If not targettable return None.
        '''
        ci, cj = self.character.position
        if self.other:
            oi, oj = self.other.position
        else:
            oi, oj = self.cell.position

        # check line-of-sight - can't shoot through other creatures
        if not los.LineOfSight(self.level, (ci, cj), (oi, oj),
                False, target=self.other):
            print 'range_check: no line of sight'
            return None

        # figure the distance
        return math.sqrt((oi-ci)**2 + (oj-cj)**2)


class MoveTo(Action):
    name = 'walk'
    type = 'move'
    label = 'Walk Here'
    description = 'Walk'
    icon = 'icon-walk.png'
    cost = 1

    def check(self, level, character, cell, other):
        if other is not None: return False

        # calculate cost now before we invoke the base check() implementation
        self.path = pathfinding.find_path(level, character.position,
            (cell.i, cell.j), ignore=(character,))
        if self.path is None:
            print 'MoveTo: no path from %s to %s'%(character.position, cell.position)
            return False

        # path costing includes current cell so drop one point
        self.cost = self.path.totalCost - 1

        if not super(MoveTo, self).check(level, character, cell, other):
            print 'MoveTo: path too expensive', self.cost, 'vs', character.actions_remaining
            return False

        return True

    def do(self, callback=None):
        def done(self=self, callback=callback):
            self.character.move_to((self.cell.i, self.cell.j), self.cost)
            if callback is not None: callback()
        sprite = self.level.character_layer.get_sprite(self.character)
        print 'ANIMATING', sprite
        self.level.do(cocos.actions.MoveTo(self.cell.center, .5) +
            cocos.actions.CallFunc(done), sprite)

class Zot(Action):
    name = 'zot'
    type = 'zot'
    label = 'Zot'
    description = 'Zot'
    cost = 1

    def check(self, level, character, cell, other):
        if not super(Zot, self).check(level, character, cell, other):
            return False

        return other is not None

    def do(self, callback=None):
        self.other.apply_damage(self.other.health)
        if callback: callback()


class Attack(Action):
    '''Attack base-class with check that "other" is a valid target.
    '''
    type = 'attack'

    def check(self, level, character, cell, other):
        if not super(Attack, self).check(level, character, cell, other):
            return False

        # I need a Character to attack
        if other is None: return False

        # No self-harm, thanks
        if other is character: return False

        # Come on, that'd just be sick
        if other.is_dead():
            print 'Melee: other is dead'
            return False

        return True

    def do(self, callback=None):
        if not self.character.use_action_points(self.cost):
            print 'Attack: not enough actions left'
        else:
            self.animate(self.do_hit, callback)

    def animate(self, result, callback):
        '''Animate the attack: default is no animation.
        '''
        result()
        if callback is not None:
            callback()

    def do_hit(self):
        if random.random() > self.chance:
            # tell the target that an attack missed
            self.other.attack_missed(self)
            return False
        damage = self.weapon.damage(self.character, self.other)
        self.other.apply_damage(damage)
        return True

    def calculate_attack_chance(self, skill):
        '''Weapon hit checks use:

            * Attacker's specific weapon skill (0 to 10) where skill 5 is
              competent
            * Difference in dexterity of attacker and defender (-10 to 10)
            * Defender's armour value (0 to 20 for full plate and beyond)
            * Light level (anything below 1/2 bright is a penalty)

        We'll use the weapon skill as the base chance to hit:

            chance = weapon_skill / 5
            chance = chance + (attacker_dex - defender_dex) / 10
            chance = chance - (armour_value / 40)
            chance = chance * min(128, light_level)/128

        Sample scenarios:

        A character with exceptional weapon skill (10) against an opponent with
        full plate armour (20) will have a 100% chance to damage, dexterity equal.

        The same person will always damage someone with no armour on, dexterity ignored.
        '''
        # base 25% chance for unskilled doing something not too challenging
        chance = 0.25

        # skill is base chance
        print 'Attack chance for', self.character, 'vs', self.other
        chance += self.character.skills.get(skill, 0) / 5.
        print '... base', skill, 'gives', chance

        # compare attacker and defender dex to account for target dodging
        chance += (self.character.dexterity - self.other.dexterity) / 10.
        print '... plus dex', self.character.dexterity, 'vs', \
            self.other.dexterity, 'gives', chance

        # account for target armour
        armour = self.other.get_armour_value()
        chance -= armour / 20.
        print '... minus armour', armour, 'gives', chance

        # Account for lighting 
        lighting = self.level.light_layer.light_map.get(self.other.position, 0)
        if self.character.has_trait('low_light_vision'):
            chance = chance * min(64, lighting)/64.
        else:
            chance = chance * min(128, lighting)/128.
        print '... light', lighting, 'gives', chance

        self.chance = min(1., max(0., chance))
        print 'Attack: chance is', self.chance

        # return the raw chance
        return chance


class Melee(Attack):
    '''Melee base-class with check that target is adjacent
    '''
    def check(self, level, character, cell, other):
        if not super(Melee, self).check(level, character, cell, other):
            return False

        if not self.adjacent_check():
            return False

        return True


class BashAttack(Melee):
    '''Special Melee attack that everyone gets.
    '''
    name = 'bash:bash'
    label = 'Bash Attack'
    description = 'Bashing attack does between 1 and strength damage'
    icon = 'icon-bash.png'
    cost = 3

    # hard-code the weapon to use
    class weapon(object):
        @staticmethod
        def damage(character, other):
            return random.randint(0, character.strength) + 1
        name = 'bash'

    def check(self, level, character, cell, other):
        if not super(BashAttack, self).check(level, character, cell, other):
            return False

        self.calculate_attack_chance('bashing')
        return True


class AnimationLayer(modal.ScrollableModalLayer):
    def __init__(self, attack, result, callback):
        super(AnimationLayer, self).__init__()
        self.attack = attack
        self.result = result
        self.callback = callback

    def on_enter(self):
        super(AnimationLayer, self).on_enter()
        if isinstance(self.attack, Magic):
            self.attack.spell.animate_attack(self, self.attack)
        else:
            self.attack.weapon.animate_attack(self, self.attack)

    def done(self):
        self.attack.level.scroller.remove(self)

    def on_exit(self):
        super(AnimationLayer, self).on_exit()
        self.result()
        if self.callback:
            self.callback()


class MeleeAttack(Melee):
    '''Melee attack with a weapon.
    '''
    def __init__(self, weapon):
        self.weapon = weapon
        self.name = '%s:%s'%(weapon.type, weapon.name)
        self.cost = weapon.action_cost
        self.label = 'Attack with %s'%self.weapon.label
        self.description = 'Attacking with "%s"'%self.weapon.description
        # XXX rename sprite_name to icon
        self.icon = weapon.sprite_name

    def check(self, level, character, cell, other):
        if not super(MeleeAttack, self).check(level, character, cell, other):
            return False

        self.calculate_attack_chance('weapon_' + self.weapon.type)
        return True

    def animate(self, result, callback):
        '''Animate the attack: default is no animation.
        '''
        self.level.scroller.add(AnimationLayer(self, result, callback), z=10)


class RangedAttack(Attack):
    '''Ranged attack with a weapon.

    Ranged weapon chance to hit is affected by distance and lighting.

    Adjacent opponents are not targettable.
    '''
    def __init__(self, weapon):
        self.weapon = weapon
        self.name = '%s:%s'%(weapon.type, weapon.name)
        self.cost = weapon.action_cost
        self.label = 'Attack with %s'%self.weapon.label
        self.description = 'Attacking with "%s"'%self.weapon.description
        # XXX rename sprite_name to icon
        self.icon = weapon.sprite_name

    def check(self, level, character, cell, other):
        if not super(RangedAttack, self).check(level, character, cell, other):
            return False

        self.distance = self.range_check()
        if self.distance == None:
            return False

        # returns the raw, unbounded chance
        chance = self.calculate_attack_chance('weapon_' + self.weapon.type)

        # harder to hit someone that's too close
        if self.distance < 2:
            self.chance = max(0, min(1, chance - .5))
            print '... minus %s is too close', self.distance, 'gives', self.chance

        return True

    def animate(self, result, callback):
        '''Animate the attack: default is no animation.
        '''
        self.level.scroller.add(AnimationLayer(self, result, callback), z=10)


class Magic(Action):
    type = 'magic'

    def __init__(self, spell):
        self.spell = spell
        self.cost = spell.action_cost
        self.name = 'cast %s'%spell.name
        self.label = 'Cast %s'%self.spell.name
        self.description = 'Casting with "%s"'%self.spell.description
        self.icon = spell.icon

    def check(self, level, character, cell, other):
        if not super(Magic, self).check(level, character, cell, other):
            return False

        # ask the spell what the base difficulty for this spell is
        chance = self.spell.difficulty(level, character, cell, other)

        # skill in this spell type adds 0 to 1
        chance += character.skills.get('spell_' + self.spell.type, 0) / 10.

        self.chance = min(1., max(0., chance))

        print 'Magic: chance is', self.chance

        return True

    def do(self, callback=None):
        if not self.character.use_action_points(self.spell.action_cost):
            print 'Magic: not enough actions left'
            return

        if not self.character.use_mana(self.spell.mana_cost):
            self.character.use_action_points(-self.spell.action_cost)
            print 'Attack: not enough mana left'
            return

        self.level.scroller.add(AnimationLayer(self, self.do_hit, callback), z=10)

    def do_hit(self):
        if random.random() > self.chance:
            return False

        self.spell.do(self.level, self.character, self.cell, self.other)
        return True


class MeleeMagic(Magic):
    def check(self, level, character, cell, other):
        if not super(MeleeMagic, self).check(level, character, cell, other):
            return False

        # I need a Character to attack
        if other is None: return False

        if not self.adjacent_check():
            return False

        if not self.spell.check(self):
            return False

        return True


class RangedMagic(Magic):
    def check(self, level, character, cell, other):
        if not super(RangedMagic, self).check(level, character, cell, other):
            return False

        if self.spell.requires_target and not other:
            return False

        self.distance = self.range_check()
        if self.distance == None:
            return False

        if not self.spell.check(self):
            return False

        return True

